package ee.ut.veebirakendus.engine;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;

import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;
import org.json.JSONArray;
import org.json.JSONObject;

import ee.ut.veebirakendus.bot.ChatBot;
import ee.ut.veebirakendus.bot.PokerBot;
import ee.ut.veebirakendus.model.Card;
import ee.ut.veebirakendus.model.Deck;
import ee.ut.veebirakendus.model.Hand;
import ee.ut.veebirakendus.model.Player;
import ee.ut.veebirakendus.servlet.PokerServlet;

public class GameEngine {

	private final List<Player> players = new LinkedList<Player>();
	private final Queue<Player> playerQueue = new LinkedBlockingQueue<Player>();
	private final Map<Street, Integer[]> limits = new HashMap<Street, Integer[]>();
	private List<Card> flop;
	private Card turn;
	private Card river;
	private PokerBot pokerBot = new PokerBot();
	private Logger log;
	private int dealer;
	private String response;
	private boolean gameStarted = false;
	private int playerCount = 9;
	private Street currentStreet;
	private String previousAction;
	private int pot;
	private int minBet = 2;
	private Player prevPlayer;
	private Player streetFirstPlayer;
	private Player lastRaised;
	private int lastToAct;
	private Deck deck;

	public enum Street {
		PREFLOP, FLOP, TURN, RIVER;
	}

	public GameEngine() {
		log = Logger.getLogger(PokerServlet.SERVLET_NAME);
		createLimits();
		deck = new Deck();
		log.info("GameEngine initialized");
	}

	public void createLimits() {
		limits.put(Street.PREFLOP, new Integer[] { 4 * minBet, minBet });
		limits.put(Street.FLOP, new Integer[] { 4 * minBet, minBet });
		limits.put(Street.TURN, new Integer[] { 8 * minBet, 2 * minBet });
		limits.put(Street.RIVER, new Integer[] { 8 * minBet, 2 * minBet });
	}

	public void createPlayers(JSONObject request) {
		// Empty player array from last session
		players.clear();

		// set first random dealer
		dealer = (int) (Math.random() * playerCount + 1);
		int playerIndex = Integer.parseInt((String) request.get("index"));

		String playerName = (String) request.get("name");
		log.debug("CreatePlayers params: name = " + playerName + ", index = "
				+ playerIndex);

		JSONObject response = new JSONObject();
		JSONArray playerArray = new JSONArray();
		JSONObject obj;
		Player player;

		for (int i = 1; i <= 9; i++) {
			if (i == playerIndex) {
				player = new Player(playerIndex, playerName, true);
			} else {
				player = new Player(i);
			}
			players.add(player);
			obj = new JSONObject();
			obj.put("name", player.getName());
			obj.put("index", player.getSeatIndex());
			obj.put("stack", player.getStack());
			playerArray.put(obj);
		}
		setGameStarted(true);
		log.debug("Players: " + players);
		response.put("players", playerArray);
		setResponse(response.toString());
	}

	private void arrangePlayerQueue() {
		previousAction = "newStreet";
		lastRaised = null;
		getFirstPlayer();
		boolean arranged = false;
		while (!arranged) {
			Player player = playerQueue.peek();
			if (streetFirstPlayer == player) {
				arranged = true;
			} else {
				player = playerQueue.poll();
				prevPlayer = player;
				playerQueue.offer(player);
			}
		}
	}

	private void getFirstPlayer() {
		boolean playerFound = false;
		Player player = null;
		int index = dealer - 1;
		while (!playerFound) {
			index = getNextIndex(index) % playerCount;
			player = players.get(index);
			if (playerQueue.contains(player)) {
				playerFound = true;
			}
		}
		streetFirstPlayer = player;
	}

	private void startHand(JSONObject request) {
		// Clear pot
		resetPot();
		log.debug("Pot reset");

		deck.createDeck();
		log.debug("Card Deck created");

		dealer = getNextIndex(dealer);
		log.debug("Dealer seat: " + dealer);

		// Clear players queue from last hand
		playerQueue.clear();
		for (Player player : players) {
			player.clearBets();
			player.getHand().getCards().clear();
			if (player.getHand().getStrongest() != null) {
				player.getHand().getStrongest().clear();
			}
			playerQueue.offer(player);
		}

		arrangePlayerQueue();

		dealCardsToAll();
		log.debug("Player queue: " + playerQueue);

		int index = Integer.parseInt((String) request.get("index"));
		Card firstCard = players.get(index - 1).getFirst();
		Card secondCard = players.get(index - 1).getSecond();

		JSONObject obj = new JSONObject();
		obj.put("dealer", dealer);
		obj.put("firstCard", firstCard.getShort());
		obj.put("secondCard", secondCard.getShort());

		setResponse(obj.toString());
		currentStreet = Street.PREFLOP;
		for (Player player : playerQueue) {
			cardsToHand(player);
		}
	}

	private void dealCardsToAll() {
		for (int i = 0; i < playerCount; i++) {
			Player player = playerQueue.poll();
			Card card = deck.getNextCard();
			player.setFirst(card);
			playerQueue.offer(player);
		}
		for (int i = 0; i < playerCount; i++) {
			Player player = playerQueue.poll();
			Card card = deck.getNextCard();
			player.setSecond(card);
			log.debug(player);
			playerQueue.offer(player);
		}
	}

	private Player postBlind(String blind) {
		Player player = playerQueue.poll();
		int blindVal = 0;
		if (blind.equalsIgnoreCase("small")) {
			blindVal = minBet / 2;
			incrementPot(blindVal);
		} else if (blind.equalsIgnoreCase("big")) {
			blindVal = minBet;
			incrementPot(blindVal);
			lastToAct = getNextIndex(getDealerSeat());
			lastToAct = getNextIndex(lastToAct);
		}
		player.updateBets(currentStreet, blindVal);
		lastRaised = playerQueue.peek();
		playerQueue.offer(player);
		return player;
	}

	private int getNextIndex(int index) {
		int nextIndex = index + 1;
		if (nextIndex == 9) {
			return nextIndex;
		} else {
			return nextIndex % 9;
		}
	}

	private int getLastIndex(int index) {
		int lastIndex = index - 1;
		if (lastIndex == 0) {
			return 9;
		} else {
			return lastIndex;
		}
	}

	public int getLastIndexArr(int index) {
		int lastIndex = index - 1;
		if (lastIndex == -1) {
			return 8;
		} else {
			return lastIndex;
		}
	}

	public void doAction(JSONObject request) {
		setResponse("");
		String action = request.getString("action");
		if (action.equalsIgnoreCase("gamestart")) {
			log.info("Gamestart called");
			createPlayers(request);
		} else if (action.equalsIgnoreCase("handstart")) {
			log.info("Handstart called");
			startHand(request);
		} else if (action.equalsIgnoreCase("chat")) {
			if (gameStarted) {
				setResponse(ChatBot.getRandomExpression(getRandomPlayerName())
						.toString());
			}
		} else if (action.equalsIgnoreCase("setblind")) {
			JSONObject obj = new JSONObject();
			Player blind = postBlind(request.getString("blind"));
			obj.put("index", blind.getSeatIndex());
			obj.put("stack", blind.getStack());
			obj.put("pot", getPot());
			obj.put("bet", blind.getStreetBet(currentStreet));
			prevPlayer = blind;
			setResponse(obj.toString());
		} else {
			act(action);
		}
	}

	private void act(String action) {
		log.debug("Previous: " + previousAction);
		Player activePlayer;
		if (action.equalsIgnoreCase("getaction")) {
			if (playerQueue.size() == 1) {
				activePlayer = playerQueue.poll();
				JSONObject response = new JSONObject();

				JSONObject player = new JSONObject();
				player.put("index", activePlayer.getSeatIndex());
				player.put("firstCard", activePlayer.getFirst().getShort());
				player.put("secondCard", activePlayer.getSecond().getShort());

				activePlayer.updateStack(getPot());
				response.put("stack", activePlayer.getStack());
				response.put("winner", activePlayer.getSeatIndex());
				response.put("action", "showdown");
				response.put("players", new JSONArray().put(player));

				setResponse(response.toString());
			} else if (isStreetOver()) {
				log.info("Street " + currentStreet + " over");
				arrangePlayerQueue();
				switch (currentStreet) {
				case PREFLOP:
					dealFlop();
					break;
				case FLOP:
					dealTurn();
					break;
				case TURN:
					dealRiver();
					break;
				case RIVER:
					showDown();
					break;
				}
			} else {
				activePlayer = playerQueue.peek();
				if (activePlayer.isPlayer()) {
					JSONObject response = new JSONObject();
					response.put("action", "player");
					response.put("index", activePlayer.getSeatIndex());
					if (prevPlayer.getStreetBet(currentStreet) == getRaiseLimit(currentStreet)) {
						response.put("raise", 0);
					} else {
						response.put("raise", 1);
					}
					setResponse(response.toString());
				} else {
					getBotAction(activePlayer);
				}
			}
		} else {
			activePlayer = playerQueue.poll();
			if (action.equalsIgnoreCase("call")) {
				call(activePlayer);
			} else if (action.equalsIgnoreCase("fold")) {
				fold(activePlayer);
			} else if (action.equalsIgnoreCase("raise")) {
				raise(activePlayer);
			}
			// TODO handle player removal, if he runs out of money
		}
	}

	private void cardsToHand(Player player) {
		log.info("PLAYER " + player.getSeatIndex() + " CARDSTOHAND: "
				+ player.getHand().getCards().toString());
		log.info("First " + player.getFirst().toString() + "  Second: "
				+ player.getSecond().toString());
		if (currentStreet == Street.PREFLOP) { // Add dealt cards to hand
			if (player.getHand().equals(null)) {
				player.setHand(new Hand());
			}
			if (!player.getHand().getCards().contains(player.getFirst())) {
				player.getHand().addCard(player.getFirst());
			}
			if (!player.getHand().getCards().contains(player.getSecond())) {
				player.getHand().addCard(player.getSecond());
			}
		} else if (currentStreet == Street.FLOP) { // add flopcards to hand
			if (player.getHand().equals(null)) {
				player.setHand(new Hand());
			}
			if (!player.getHand().getCards().contains(player.getFirst())) {
				player.getHand().addCard(player.getFirst());
			}
			if (!player.getHand().getCards().contains(player.getSecond())) {
				player.getHand().addCard(player.getSecond());
			}
			if (!player.getHand().getCards().containsAll(flop)) {
				for (Card card : flop) {
					player.getHand().getCards().add(card);
				}
			}
		} else if (currentStreet == Street.TURN) {// add turncard to hand
			if (player.getHand().equals(null)) {
				player.setHand(new Hand());
			}
			if (!player.getHand().getCards().contains(player.getFirst())) {
				player.getHand().addCard(player.getFirst());
			}
			if (!player.getHand().getCards().contains(player.getSecond())) {
				player.getHand().addCard(player.getSecond());
			}
			if (!player.getHand().getCards().containsAll(flop)) {
				for (Card card : flop) {
					player.getHand().getCards().add(card);
				}
			}
			if (!player.getHand().getCards().contains(turn)) {
				player.getHand().getCards().add(turn);

			}
		} else if (currentStreet == Street.RIVER) {// add rivercard to hand
			if (!player.getHand().getCards().contains(player.getFirst())) {
				player.getHand().addCard(player.getFirst());
			}
			if (!player.getHand().getCards().contains(player.getSecond())) {
				player.getHand().addCard(player.getSecond());
			}
			if (!player.getHand().getCards().containsAll(flop)) {
				for (Card card : flop) {
					player.getHand().getCards().add(card);
				}
			}
			if (!player.getHand().getCards().contains(turn)) {
				player.getHand().getCards().add(turn);
				if (!player.getHand().getCards().contains(river)) {
					player.getHand().getCards().add(river);
				}
			}
		}

	}

	public boolean isStreetOver() {
		boolean streetOver = false;
		Player player = playerQueue.peek();
		if (player.getStreetBet(currentStreet) == getRaiseLimit(currentStreet)) {
			streetOver = true;
		} else if ((lastRaised != null) && (previousAction != "newStreet")) {
			if (player == lastRaised) {
				streetOver = true;
			}
		} else if ((player == streetFirstPlayer)
				&& (previousAction != "newStreet")) {
			streetOver = true;
		}

		return streetOver;
	}

	public void showDown() {
		JSONObject response = new JSONObject();
		JSONArray playersJSON = new JSONArray();
		JSONObject obj;
		Player player;
		while (!playerQueue.isEmpty()) {
			player = playerQueue.poll();
			obj = new JSONObject();
			obj.put("index", player.getSeatIndex());
			obj.put("firstCard", player.getFirst().getShort());
			obj.put("secondCard", player.getSecond().getShort());
			playersJSON.put(obj);
		}
		Player winner = pokerBot.compareAllHands(players);
		winner.updateStack(getPot());
		log.info("Winner hand: " + winner.getHand().getRank());
		log.info(winner);
		response.put("winner", winner.getSeatIndex());
		response.put("stack", winner.getStack());
		response.put("action", "showdown");
		response.put("players", playersJSON);
		setResponse(response.toString());
	}

	public void sendResponse(HttpServletResponse resp) throws IOException {
		PrintWriter pw = resp.getWriter();
		if (log.isDebugEnabled()) {
			log.debug("Response: " + getResponse());
		}
		pw.write(getResponse());
		pw.close();
	}

	public void getBotAction(Player player) {
		if (currentStreet == Street.PREFLOP) {
			act(pokerBot.getPreFlopAction(player, previousAction));
		} else {
			act(pokerBot.getAction(player, previousAction));
		}
	}

	public void call(Player player) {
		int prevBet = prevPlayer.getStreetBet(currentStreet);
		int currBet = player.getStreetBet(currentStreet);
		int bet = prevBet - currBet;
		player.updateBets(currentStreet, bet);
		incrementPot(bet);
		JSONObject obj = new JSONObject();
		obj.put("action", "call");
		obj.put("index", player.getSeatIndex());
		obj.put("bet", bet);
		obj.put("chips", prevBet);
		obj.put("stack", player.getStack());
		playerQueue.offer(player);
		prevPlayer = player;
		setResponse(obj.toString());
		if (previousAction != "raise") {
			previousAction = "call";
		}
	}

	public void raise(Player player) {
		int prevBet = prevPlayer.getStreetBet(currentStreet);
		if (prevBet == getRaiseLimit(currentStreet)) {
			previousAction = "call";
			lastToAct = getLastIndex(player.getSeatIndex());
			call(player);
		} else {
			lastToAct = getLastIndex(player.getSeatIndex());
			int step = getRaiseStep(currentStreet);
			int currBet = player.getStreetBet(currentStreet);
			int bet = (prevBet + step) - currBet;
			player.updateBets(currentStreet, bet);
			incrementPot(bet);
			JSONObject obj = new JSONObject();
			obj.put("action", "raise");
			obj.put("index", player.getSeatIndex());
			obj.put("bet", bet);
			obj.put("stack", player.getStack());
			playerQueue.offer(player);
			prevPlayer = player;
			setResponse(obj.toString());
			previousAction = "raise";
			lastRaised = player;
		}
	}

	public void fold(Player player) {
		JSONObject obj = new JSONObject();
		obj.put("action", "fold");
		obj.put("index", player.getSeatIndex());
		if (lastRaised != null && lastRaised == player) {
			lastRaised = playerQueue.peek();
		}
		setResponse(obj.toString());
	}

	public void setDealerSeat(int dealerSeat) {
		this.dealer = dealerSeat;
	}

	public void dealFlop() {
		log.info("Deal flop!");
		flop = new ArrayList<Card>();
		deck.getNextCard();
		int flopLength = 3;
		JSONArray arr = new JSONArray();
		for (int i = 0; i < flopLength; i++) {
			Card card = deck.getNextCard();
			flop.add(card);
			arr.put(card.getShort());
		}
		JSONObject obj = new JSONObject();
		obj.put("action", "flop");
		obj.put("cards", arr);
		setResponse(obj.toString());
		currentStreet = Street.FLOP;
		for (Player player : playerQueue) {
			cardsToHand(player);
			pokerBot.giveRank(player.getHand());
		}
	}

	public void dealRiver() {
		log.info("Deal river!");
		deck.getNextCard();
		river = deck.getNextCard();
		JSONObject obj = new JSONObject();
		obj.put("action", "river");
		obj.put("card", river.getShort());
		setResponse(obj.toString());
		currentStreet = Street.RIVER;
		for (Player player : playerQueue) {
			cardsToHand(player);
			pokerBot.giveRank(player.getHand());
		}
	}

	public void dealTurn() {
		log.info("Deal turn!");
		deck.getNextCard();
		turn = deck.getNextCard();
		JSONObject obj = new JSONObject();
		obj.put("action", "turn");
		obj.put("card", turn.getShort());
		setResponse(obj.toString());
		currentStreet = Street.TURN;
		for (Player player : playerQueue) {
			cardsToHand(player);
			pokerBot.giveRank(player.getHand());
		}
	}

	public int getDealerSeat() {
		return dealer;
	}

	private void setResponse(String response) {
		this.response = response;
	}

	private String getResponse() {
		return response;
	}

	private void resetPot() {
		pot = 0;
	}

	private int getPot() {
		return pot;
	}

	private void incrementPot(int value) {
		pot += value;
	}

	public void setGameStarted(boolean gameStarted) {
		this.gameStarted = gameStarted;
	}

	public boolean isGameStarted() {
		return gameStarted;
	}

	public int getRaiseLimit(Street street) {
		return limits.get(street)[0];
	}

	public int getRaiseStep(Street street) {
		return limits.get(street)[1];
	}

	public String getRandomPlayerName() {
		Player player;
		do {
			int random = (int) (Math.random() * playerCount);
			player = players.get(random);
		} while (!player.getName().contains("Player"));
		return player.getName();
	}
}
